package jsr292.java.lang.invoke;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import jsr292.weaver.Utils;

import org.objectweb.asm.Type;

public class MethodType implements Serializable {
    final Class<?>[] parameterTypes;
    final Class<?> returnType;
    
    private static final long serialVersionUID = 292L;
    
    /** Constructor is private to enforce use of interning factory method. */
    private MethodType(Class<?> returnType, Class<?>[] parameterTypes) {
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
    }

    @Override
    public int hashCode() {
        return returnType.hashCode() ^ Arrays.hashCode(parameterTypes);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof MethodType))
            return false;
        MethodType methodType=(MethodType)obj;
        return returnType == methodType.returnType &&
               Arrays.equals(parameterTypes, methodType.parameterTypes);
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append('(');
        if (parameterTypes.length != 0) {
            for(Class<?> type : parameterTypes) {
                appendName(builder, type).append(", ");
            }
            builder.setLength(builder.length() - 2);
        }
        return appendName(builder.append(')'), returnType).toString();
    }
    
    private static StringBuilder appendName(StringBuilder builder, Class<?> clazz) {
        if (clazz.isArray())
            return appendName(builder, clazz.getComponentType()).append("[]");
        return builder.append(clazz.getName());
    }

    public Class<?> parameterType(int num) {
        return parameterTypes[num];
    }
    
    public int parameterCount() {
        return parameterTypes.length;
    }
    
    public Class<?> returnType() {
        return returnType;
    }
    
    public List<Class<?>> parameterList() {
        return Collections.unmodifiableList(Arrays.asList(parameterTypes));
    }

    public Class<?>[] parameterArray() {
        return parameterTypes.clone();
    }
    
    
    // --- helpers
    
    public MethodType generic() {
        return genericMethodType(parameterTypes.length);
    }
    
    public static
    MethodType genericMethodType(int objectArgCount, boolean varargs) {
        Class<?>[] types=new Class<?>[objectArgCount + ((varargs)?1:0)];
        Arrays.fill(types, 0, objectArgCount, Object.class);
        if (varargs) {
            types[objectArgCount] = Object[].class;
        }
        return makeInternal(Object.class, true, types);
    }

    public static
    MethodType genericMethodType(int objectArgCount) {
        return genericMethodType(objectArgCount, false);
    }

    public MethodType changeParameterType(int index, Class<?> nptype) {
        if (parameterTypes[index] == nptype) {
            return this;
        }
        Class<?>[] types = parameterTypes.clone();
        types[index] = nptype;
        return makeInternal(returnType, true, types);
    }
    
    MethodType unpermuteOnInsert(int index, int valueLength) {
      int length = parameterTypes.length;
      Class<?>[] types=new Class<?>[length];  
        
      for(int i=0; i<valueLength; i++) {
          types[i] = parameterTypes[i + index];
      }
      for(int i=0; i<index; i++) {
          types[i + valueLength] = parameterTypes[i];
      }
      for(int i=index + valueLength; i< parameterTypes.length; i++) {
          types[i] = parameterTypes[i];
      }
          
      
      /*
      int length = parameterTypes.length;
      Class<?>[] types=new Class<?>[length];
      for(int i=0; i<length; i++) {
          int order = reorder[i];
          if (order<0  || order>= length) {
              throw new IllegalArgumentException("order "+Arrays.toString(reorder)+" not in range at index "+i);
          }
          types[i] = parameterTypes[order];
      }
      */
      return makeInternal(returnType, true, types);
    }

    public MethodType appendParameterTypes(Class<?>... newParameterTypes) {
        int length = newParameterTypes.length;  // NPE
        if (length == 0) {
            return this;
        }
        Class<?>[] types = ArrayUtils.copyOf(parameterTypes, parameterTypes.length + length);
        System.arraycopy(newParameterTypes, 0, types, length, length);
        return makeInternal(returnType, true, types);
    }
    
    public MethodType appendParameterTypes(List<Class<?>> newParameterTypes) {
        int size = newParameterTypes.size();   // NPE
        if (size == 0) {
            return this;
        }
        return appendParameterTypes(newParameterTypes.toArray(new Class<?>[size]));
    }
    
    public MethodType insertParameterTypes(int index, Class<?>... newParameterTypes) {
        int newTypesLength = newParameterTypes.length;  // NPE before IAE
        int length = parameterTypes.length;
        if (index < 0 || index > length)
            throw new IllegalArgumentException("index="+index);
        
        Class<?>[] types = ArrayUtils.copyOfRange(parameterTypes, 0, length + newTypesLength);
        System.arraycopy(types, index, types, index + newTypesLength, length - index);
        System.arraycopy(newParameterTypes, 0, types, index, newTypesLength);
        return makeInternal(returnType, true, types);
    }
    
    public MethodType insertParameterTypes(int num, List<Class<?>> newParameterTypes) {
        return insertParameterTypes(num, newParameterTypes.toArray(new Class<?>[newParameterTypes.size()]));
    }

    // used by JavaMethodHandle and foldArguments
    MethodType dropParameterType(int index) {
        int parameterTypeLength = parameterTypes.length;
        Class<?>[] types;
        if (index == 0) {
            types = ArrayUtils.copyOfRange(parameterTypes, 1, parameterTypeLength);
        } else {
            types = ArrayUtils.copyOf(parameterTypes, parameterTypeLength - 1);
            System.arraycopy(parameterTypes, index + 1, types, index, parameterTypeLength - 1 - index);
        }
        return makeInternal(returnType, true, types);
    }
    
    // used by insertArguments
    public MethodType dropParameterTypes(int start, int end) {
      int length = parameterTypes.length;
      if (start < 0 || start > end || end > length)
          throw new IllegalArgumentException("start:"+start+" end:"+end);
      
      Class<?>[] types;
      if (start == 0) {
          types = ArrayUtils.copyOfRange(parameterTypes, end, length);
      } else {
          if (end == length) {
              types = ArrayUtils.copyOfRange(parameterTypes, 0, start);
          } else {
              int diff = length - end;
              types = ArrayUtils.copyOfRange(parameterTypes, 0, start + diff);
              System.arraycopy(parameterTypes, end, types, start, diff);
          }
      }
      return makeInternal(returnType, true, types);
    }

    public MethodType changeReturnType(Class<?> newReturnType) {
        if (returnType == newReturnType)  return this;
        return makeInternal(newReturnType, true, parameterTypes);
    }

   
    
    public boolean hasPrimitives() {
        if (returnType.isPrimitive()) {
            return true;
        }
        for(Class<?> type:parameterTypes) {
            if (type.isPrimitive()) {
                return true;
            }
        }
        return false;
    }

    public boolean hasWrappers() {
        if (primitiveOf(returnType) != null) {
            return true;
        }
        for(Class<?> type:parameterTypes) {
            if (primitiveOf(type) != null) {
                return true;
            }
        }
        return false;
    }
    
    private static Class<?> primitiveOf(Class<?> clazz) {
        String name=clazz.getName();
        int length = name.length();
        if (length<14) // at least java.lang.Long/Byte
            return null;
        switch(name.charAt(length-1)) {
        case 'n': // boolean
            if ("java.lang.Boolean".equals(name))
                return boolean.class;
            return null;
        case 'e': // byte or double
            if ("java.lang.Double".equals(name))
                return double.class;
            if ("java.lang.Byte".equals(name))
                return byte.class;
            return null;
        case 't': // short or float
            if ("java.lang.Float".equals(name))
                return float.class;
            if ("java.lang.Short".equals(name))
                return short.class;
            return null;
        case 'r': // int
            if ("java.lang.Integer".equals(name))
                return int.class;
            return null;
        case 'g': // long
            if ("java.lang.Long".equals(name))
                return long.class;
            return null;
        case 'd': // void
            if ("java.lang.Void".equals(name))
                return void.class;
            return null;
        }
        return null;
    }
    
    private static Class<?> wrapperOf(Class<?> clazz) {
        String name=clazz.getName();
        if (name.length()<2)
            return null;
        
        switch(name.charAt(0)<<8+name.charAt(1)) {
        case 'b'<<8+'o': // boolean
            return Boolean.class;
        case 'b'<<8+'y': // byte
            return Byte.class;
        case 'c'<<8+'h': // char
            return Character.class;
        case 'd'<<8+'o': // double
            return Double.class;
        case 'f'<<8+'l': // float
            return Float.class;
        case 'i'<<8+'n': // int
            return Integer.class;
        case 'l'<<8+'o': // long
            return Long.class;
        case 's'<<8+'h': // short
            return Short.class;
        case 'v'<<8+'o': // void
            return Void.class;
        }
        return null;
    }

    public MethodType erase() {
        return erase(returnType, parameterTypes);
    }
    
    private static MethodType erase(Class<?> returnType, Class<?>[] parameterTypes) {
        Class<?>[] types=new Class<?>[parameterTypes.length];
        for(int i=0; i<types.length; i++) {
            types[i] = erase(parameterTypes[i]);
        }
        returnType = erase(returnType);
        return makeInternal(returnType, true, types);
    }
    
    static Class<?> erase(Class<?> type) {
        if (type.isPrimitive() || type==void.class)
            return type;
        return Object.class;
    }

    public MethodType wrap() {
        Class<?>[] types=new Class<?>[parameterTypes.length];
        for(int i=0; i<types.length; i++) {
            types[i] = wrap(parameterTypes[i]);
        }
        Class<?> rType = wrap(returnType);
        return makeInternal(rType, true, types);
        
    }
    
    private static Class<?> wrap(Class<?> type) {
       Class<?> wrapper = wrapperOf(type);
       return (wrapper != null)? wrapper: type;  
    } 

    public MethodType unwrap() {
        Class<?>[] types=new Class<?>[parameterTypes.length];
        for(int i=0; i<types.length; i++) {
            types[i] = unwrap(parameterTypes[i]);
        }
        Class<?> rType = unwrap(returnType);
        return makeInternal(rType, true, types);
    }
    
    private static Class<?> unwrap(Class<?> type) {
        Class<?> primitive = primitiveOf(type);
        return (primitive != null)? primitive: type;  
     } 
    

    private static final HashMap<MethodType,MethodType> METHOD_TYPE_CACHE=
        new HashMap<MethodType,MethodType>();

    static MethodType makeInternal(Class<?> returnType, boolean trust, Class<?>... parameterTypes) {
        MethodType query=new MethodType(returnType, parameterTypes);
        MethodType result;
        synchronized(METHOD_TYPE_CACHE) {
            result = METHOD_TYPE_CACHE.get(query);
            if (result != null) {
                return result;
            }
            if (!trust) {
                parameterTypes=parameterTypes.clone(); // defensive copy if needed
            }
            result = new MethodType(returnType, parameterTypes); 
            METHOD_TYPE_CACHE.put(result, result);
        }
        return result;
    }
    
    private Object readResolve() {
        // don't trust to avoid forged serialization blob that share parameterTypes
        return makeInternal(returnType, false, parameterTypes);
    }
    
    private static final Class<?>[] EMPTY_ARRAY_TYPE=new Class<?>[0];

    public static MethodType methodType(Class<?> returnType, Class<?>[] parameterTypes) {
        return makeInternal(returnType, false, parameterTypes);
    }

    public static MethodType methodType(Class<?> returnType, Class<?> receiver, Class<?>... parameterTypes) {
        Class<?>[] types = new Class<?>[parameterTypes.length + 1];
        System.arraycopy(parameterTypes, 0, types, 1, parameterTypes.length);
        types[0] = receiver;
        return makeInternal(returnType, true, types);
    }

    public static MethodType methodType(Class<?> returnType, List<? extends Class<?>> parameterTypes) {
        return makeInternal(returnType, true, parameterTypes.toArray(EMPTY_ARRAY_TYPE));
    }

    public static MethodType methodType(Class<?> returnType) {
        return makeInternal(returnType, true, EMPTY_ARRAY_TYPE);
    }

    public static MethodType methodType(Class<?> rtype, Class<?> parameterType) {
        return makeInternal(rtype, true, new Class<?>[]{ parameterType });
    }

    public static MethodType methodType(Class<?> rtype, MethodType methodType) {
        return makeInternal(rtype, true, methodType.parameterTypes);
    }

    /*
    public int parameterSlotCount() {
        int slot=0;
        for(Class<?> type:parameterTypes) {
            slot += (type==long.class || type==double.class)?2:1;
        }
        return slot;
    }

    public int parameterSlotDepth(int num) {
        throw new UnsupportedOperationException();
    }

    public int returnSlotCount() {
        return (returnType==long.class || returnType==double.class)?2:1;
    }*/

    public static MethodType fromMethodDescriptorString(String descriptor, ClassLoader loader)
      throws IllegalArgumentException, TypeNotPresentException {
        
        Type[] types = Type.getArgumentTypes(descriptor);
        Class<?>[] parameterTypes = new Class<?>[types.length];
        for(int i=0; i<parameterTypes.length;i++) {
            parameterTypes[i] = toClass(types[i], loader);
        }
        Class<?> returnType = toClass(Type.getReturnType(descriptor), loader);
        return makeInternal(returnType, true, parameterTypes);
    }
    
    private static Class<?> toClass(Type type, ClassLoader loader) {
        switch(type.getSort()) {
        case Type.ARRAY:
            return Array.newInstance(toClass(type.getElementType(), loader), 0).getClass();
        case Type.BOOLEAN:
            return boolean.class;
        case Type.BYTE:
            return byte.class;
        case Type.CHAR:
            return char.class;
        case Type.DOUBLE:
            return double.class;
        case Type.FLOAT:
            return float.class;
        case Type.INT:
            return int.class;
        case Type.LONG:
            return long.class;
        case Type.SHORT:
            return short.class;
        case Type.VOID:
            return void.class;
        default: // TYPE.OBJECT
            String className = type.getClassName();
            if (className.startsWith(Utils.JAVA_LANG_INVOKE_DOT_PREFIX)) {
                className = "jsr292."+className;
            }
            
            try {
                if (loader == null)
                    return Class.forName(className);
                return loader.loadClass(className);
            } catch (ClassNotFoundException e) {
                throw new TypeNotPresentException(className, e);
            }
        }
    }

    public String toMethodDescriptorString() {
        Type[] types=new Type[parameterTypes.length];
        for(int i=0;i<types.length;i++) {
            types[i] = Type.getType(parameterTypes[i]);
        }
        Type returnType=Type.getType(this.returnType);
        return Type.getMethodDescriptor(returnType, types);
    }
}
